/* TableWidget.js
 *
 * Copyright 2006, Tim Dwelle.
 *
 * Licensed under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in
 * compliance with the License.  You may obtain a copy of
 * the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in
 * writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
 * CONDITIONS OF ANY KIND, either express or implied.  See
 * the License for the specific language governing
 * permissions and limitations under the License.
 *
 */

$package("dowry.widget");

/**
 * Object representation of a widget representing a table.
 *
 */
$class("TableWidget").$extends("Widget").$with("Transformable").$as(
{
    /**
     * Constant for the CSS class name to denote an
     * alternate table row.
     *
     */
    CSS_ALTERNATE_ROW : "alternate",

    /**
     * An array of functions that are used to generate the
     * cell contents. if you simply want to display a simple
     * property of an object in the array of values, you can
     * pass in the string property name instead of a
     * function.  If an element of the array is null, it
     * will be handled using the default cell handler.
     *
     */
    cells : null,

    /**
     * Property indicating the ids of the table's header
     * columns.
     *
     */
    _cols : null,

    /**
     * Property indicating the body of the table.
     *
     */
    _tbody : null,

    /**
     * Adds the specified values to the body of the table.
     *
     * <p>
     * The rows created will be based on the datatypes
     * bound to the "column headers" in the table's head.
     * The cell will be populated with the field of each
     * element that matches the datatype's <code>property
     * </code> property.
     * </p>
     *
     * @param vals  an array of object values containing
     *              the appropriate fields for the table's
     *              columns
     *
     */
    addValues : function(vals)
    {
        vals = this.transform(vals);
        var tbody = this.getTableBody();

        if (tbody != null &&
            vals != null)
        {
            // determine how many rows we already have
            // in this section of the table
            var len = tbody.rows.length;

            for (var i=0; i < vals.length; i++)
            {
                var rowIndex = i+len;
                var tr = this._addValue(vals[i], rowIndex);

                // add zebra-striping to the table
                // body rows
                this._zebraStripe(tr, rowIndex);

                if (tr != null)
                {
                    tbody.appendChild(tr);
                }
            }
        }
    },

    /**
     * Creates <code>&lt;td&gt;</code> element.
     *
     * @param val        the value of the row
     *
     * @param colIndex   the index of the column
     *
     * @param rowIndex   the index of the row
     *
     */
    createCell : function(val, colIndex, rowIndex)
    {
        return document.createElement("td");
    },

    /**
     * Creates a <code>&lt;tr&gt;</code> element.
     *
     * @param val        the value of the row
     *
     * @param rowIndex   the index of the row
     *
     */
    createRow : function(val, rowIndex)
    {
        return document.createElement("tr");
    },

    /**
     * Gets an array of string IDs of the column header
     * cells in the table's head.
     *
     * @return  the IDs for each column
     *
     */
    getColumns : function()
    {
        var cols = this._cols;

        if (cols == null)
        {
            var ele = this.getElement();

            if (ele != null &&
                ele.tHead != null)
            {
                var thead = ele.tHead;

                // loop through all the rows in the
                // head, looking for element's with
                // bound datatypes
                var rows = thead.rows;
                if (rows != null)
                {
                    for (var i = 0; i < rows.length; i++)
                    {
                        var cells = rows[i].cells;
                        for (var j = 0; j < cells.length; j++)
                        {
                            var cell = cells[j];

                            // if we find one... add it to the
                            // list
                            if (cell != null &&
                                cell.id != null &&
                                cell.dowryDatatype != null)
                            {
                                if (cols == null)
                                {
                                    cols = new Array();
                                }

                                cols[cell.cellIndex] = cell.id;
                            }
                        }
                    }
                }
            }

            // make sure that none of the array
            // elements are null
            if (cols != null)
            {
                for (var i=0; i < cols.length; i++)
                {
                    if (cols[i] == null)
                    {
                        cols = null;
                        break;
                    }
                }
            }

            this._cols = cols;
        }

        return cols;
    },

    /**
     * Gets the table body for this table where "the action
     * happens".
     *
     * @return  the tbody element for this table
     *
     */
    getTableBody : function()
    {
        var tbody = this._tbody;

        if (tbody == null)
        {
            var ele = this.getElement();

            if (ele != null &&
                ele.tbodies != null &&
                ele.tbodies[0] != null)
            {
                tbody = ele.tbodies[0];
            }
            else
            {
                tbody = document.createElement("tbody");
                ele.appendChild(tbody);

                // apparently the append child doesn't
                // update the tbodies collection...
                // when it doesn't, we will add the
                // collection manually
                if (ele.tbodies != null)
                {
                    ele.tbodies = [ tbody ];
                }

                this._tbody = tbody;
            }
        }

        return tbody;
    },

    /**
     * Gets the value of the bound element.  In the case of
     * a table, however, this is not applicable... null will
     * always be returned.
     *
     * @return  the value
     *
     */
    getValue : function()
    {
        return null;
    },

    /**
     * Removes all values from the body of the table.
     *
     */
    removeAllValues : function()
    {
        var tbody = this.getTableBody();
        if (tbody != null)
        {
            while (tbody.childNodes.length > 0)
            {
                tbody.removeChild(tbody.firstChild);
            }
        }
    },

    /**
     * Sets the value of the bound element.  In the case of
     * a table, however, this is not applicable... this
     * method does nothing.
     *
     * @param val  the value to set
     *
     */
    setValue : function(val)
    {
    },

    /**
     * Sorts the specified column of the table.
     *
     * @param col    ID of the column header cell in the
     *               table's head
     *
     */
    sort : function(col)
    {
        col = $(col);

        if (col != null &&
            col.dowryDatatype != null)
        {
            // get the location of this cell
            var colIndex = col.cellIndex;
            var tbody = this.getTableBody();

            if (tbody != null &&
                tbody.rows != null &&
                colIndex != null)
            {
                // get the sortable rows of the table
                var sortedRows = new Array();
                for (var i=0; i < tbody.rows.length; i++)
                {
                    sortedRows[sortedRows.length] = tbody.rows[i];
                }

                // sort these rows
                var func = this._sortComparator(col.dowryDatatype, colIndex);
                sortedRows.sort(func);

                // account for sort order
                if (!col._sortOrder)
                {
                    col._sortOrder = true;
                }
                else
                {
                    sortedRows.reverse();
                    col._sortOrder = false;
                }

                // update table
                for (var i=0;i<sortedRows.length;i++)
                {
                    // add zebra-striping to the table
                    // body rows
                    this._zebraStripe(sortedRows[i], i);

                    tbody.appendChild(sortedRows[i]);
                }
            }
        }
    },

    /**
     * Starts a widget by associating a handler for each
     * column header, so clicking on it initiates a sort.
     *
     */
    start : function()
    {
        var cols = this.getColumns();
        if (cols != null)
        {
            for (var i=0; i < cols.length; i++)
            {
                var col = cols[i];
                var func = this.sort.callback(col);
                Dowry.event.addEvent(col, "click", func);
            }
        }
    },

    /**
     * Adds a single row to the table.
     *
     * @param val        the value for the row
     *
     * @param rowIndex   the index of the row
     *
     */
    _addValue : function(val, rowIndex)
    {
        // generate our tr
        var tr = this.createRow(val, rowIndex);
        if (tr != null)
        {
            var cols = this.getColumns();
            if (cols != null)
            {
                for (var colIndex=0; colIndex < cols.length; colIndex++)
                {
                    var col = cols[colIndex];
                    var dt = $(col).dowryDatatype;

                    var s = null;
                    if (col != null &&
                        dt != null)
                    {
                        // no array of cell handlers, or no
                        // cell handler for this column...
                        // try to determine the defaults
                        if (this.cells == null ||
                            (typeof this.cells[colIndex] != "string" &&
                             typeof this.cells[colIndex] != "function"))
                        {
                            // try to use the property name specified
                            // on the datatype... but, in a pinch, we
                            // can always use the datatype's id
                            var property = dt.property;
                            if (property == null)
                            {
                                property = col;
                            }

                            // determine the cell's value... unlike
                            // many components, we *need* a datatype
                            if (val != null &&
                                property != null)
                            {
                                s = val[property];
                            }
                        }

                        // cell handler is a property
                        else if (typeof this.cells[colIndex] == "string")
                        {
                            s = this.cells[colIndex];
                        }

                        // cell handler is a function
                        else if (typeof this.cells[colIndex] == "function")
                        {
                            var func = this.cells[colIndex];
                            s = func.call(null, val);
                        }

                        // format raw value... if the raw
                        // value returned is an element,
                        // then we will trust that they
                        // already properly formatted that
                        // element
                        if (s != null && s.nodeType != 1)
                        {
                            s = dt.toFormattedString(s);
                        }
                    }

                    // generate our td
                    var td = this.createCell(val, colIndex, rowIndex);

                    // if the cell creator doesn't create a
                    // td, we can't skip it... or else the
                    // table will look wonky... we'll need
                    // to create a default td
                    if (td == null)
                    {
                        td = document.createElement("td");
                    }

                    this._setValueImpl(td, s);
                    tr.appendChild(td);
                }
            }
        }

        // row creator didn't return a tr...
        // guess we skip this row
        else
        {
            tr = null;
        }

        return tr;
    },

    /**
     * Returns a function that calls the datatype's
     * <code>compare()</code> function.
     *
     * @param dt        the datatype to use for comparison
     *
     * @param colIndex  the index of the column being sorted
     *
     * @return          a function to use as the sort
     *                  comparator
     *
     */
    _sortComparator : function(dt, colIndex)
    {
        var widget = this;
        return function(row1, row2)
        {
            var rc = -1;

            // we are being passed in the row to
            // be compared... so we need to get
            // the contents of the relevant cell
            var a = widget._getValueImpl(row1.cells[colIndex]);
            var b = widget._getValueImpl(row2.cells[colIndex]);

            // convert the raw value to the
            // appropriate datatype
            var value1 = dt.toDatatype(a);
            var value2 = dt.toDatatype(b);

            // if the raw value wasn't null, then
            // a null converted value must mean
            // that it could not be converted to
            // the appropriate datatype
            var match1 = (a == null) == (value1 == null);
            var match2 = (b == null) == (value2 == null);

            // sort non-convertible values to
            // the bottom
            if (!match1 && !match2)
            {
                rc = 0;
            }
            else if (!match1)
            {
                rc = 1;
            }
            else if (!match2)
            {
                rc = -1;
            }

            // otherwise... compare based on the
            // converted value
            else if (value1 == value2)
            {
                rc = 0;
            }
            else if (value1 == null)
            {
                rc = -1;
            }
            else if (value2 == null)
            {
                rc = 1;
            }
            else
            {
                rc = dt.compare(value1, value2);
            }

            return rc;
        }
    },

    /**
     * Adds zebra-striping to the table row.
     *
     * @param tr         the row to stripe
     *
     * @param rowIndex   the index of the row
     *
     */
    _zebraStripe : function(tr, rowIndex)
    {
        if (tr != null &&
            rowIndex != null)
        {
            if (rowIndex%2 == 0)
            {
                Dowry.css.removeClassName(tr, this.CSS_ALTERNATE_ROW);
            }
            else
            {
                Dowry.css.addClassName(tr, this.CSS_ALTERNATE_ROW);
            }
        }
    }
});
